﻿#pragma once

#include "common.h"
#include "amath.h"
#include <list>

// SFINAE helper for detecting types with arithmetic operators
// usage:
//     template<typename T = TypeToDetect> void fun(T a, ..., _Specialization, _HasArithmetic(T));
//     template<typename T = TypeToDetect> void fun(T a, ..., _Fallback);
//
//     fun(args, _Specialization()); // when called
#define _HasArithmetic(T) \
	std::decay_t<decltype(std::declval<T>() + std::declval<T>())>* = nullptr, \
	std::decay_t<decltype(std::declval<T>() / 1.0f)>* = nullptr, \
	std::decay_t<decltype(std::declval<T>() * 1.0f)>* = nullptr

namespace anip
{
	enum class Interpolation
	{
		Linear,
		Discrete,
		CubicHermite
	};

	enum class TemporalTiling
	{
		None,
		Clamp,
		Extrapolate,
		Repeat,
		Mirror
	};
	
	template<typename T>
	class KeyedValue;
	
	template<typename T>
	void binaryWrite(const KeyedValue<T>& val, std::ostream& o);
	template<typename T>
	void binaryRead(KeyedValue<T>& val, std::istream& in);

	template<typename T>
	class KeyedValue
	{
		friend void anip::binaryWrite<T>(const KeyedValue<T>&, std::ostream&);
		friend void anip::binaryRead<T>(KeyedValue<T>&, std::istream&);

	public:
		class Keypoint
		{
			friend class KeyedValue;
			friend void anip::binaryWrite<T>(const KeyedValue<T>&, std::ostream&);
			friend void anip::binaryRead<T>(KeyedValue<T>&, std::istream&);

		private:
			Keypoint* _prev = nullptr;
			Keypoint* _next = nullptr;

		public:
			float time;
			mutable T value{};
			mutable Interpolation type;
			mutable T slope{};
			mutable float weight = 1;

			const Keypoint* previous() const { return _prev; }
			const Keypoint* next() const { return _next; }

			Keypoint(float t = 0, const T& v = T(), Interpolation i = Interpolation::Linear)
				: time(t), value(v), type(i) {}
		};

	private:
		Keypoint *_head = nullptr, *_tail = nullptr;
		size_t _size = 0;
		float _start = 0, _end = 0;

		template<typename U = T>
		static U getSlope(const Keypoint& a, const Keypoint& b, 
			_Specialization, _HasArithmetic(U))
		{
			return (b.value - a.value) / (b.time - a.time);
		}

		template<typename U = T>
		static U getSlope(const Keypoint& a, const Keypoint& b, _Fallback)
		{
			// subsequent interpolation will always fail, so we don't
			// really care about what this returns
			return a.value;
		}

		template<typename U = T>
		void _smoothen(const Keypoint& kp, float fac, _Specialization, _HasArithmetic(U)) const
		{
			if (kp._prev == nullptr && kp._next == nullptr) return;

			U target{};
			if (kp._prev == nullptr)
			{
				// if tiling is 'mirror', then the target should (hopefully) be always zero
				if (tiling != TemporalTiling::Mirror)
					target = getSlope(kp, *kp._next, _Specialization());
			}
			else if (kp._next == nullptr)
			{
				if (tiling != TemporalTiling::Mirror)
					target = getSlope(*kp._prev, kp, _Specialization());
			}
			else
			{
				float t1 = kp._prev->time, t2 = kp.time, t3 = kp._next->time;
				target = math::linearInterpolate(
					getSlope(*kp._prev, kp, _Specialization()),
					getSlope(kp, *kp._next, _Specialization()),
					(t3 - t2) / (t3 - t1));
			}
			kp.slope = math::linearInterpolate(kp.slope, target, fac);
		}

		template<typename U = T>
		void _smoothen(const Keypoint& kp, float fac, _Fallback) const {}

		template<typename U = T>
		static U _interpolate(const Keypoint& p1, const Keypoint& p2, float t, 
			_Specialization, _HasArithmetic(U))
		{
			U d0, d1;
			switch (p1.type)
			{
			case Interpolation::Linear:
				if (p2.type == Interpolation::CubicHermite)
				{
					d0 = getSlope(p1, p2, _Specialization());
					d1 = p2.slope;
					return math::cubicHermiteInterpolate(
						p1.time, p1.value, p2.time, p2.value, d0, d1, t);
				}
				else return math::linearInterpolate(
					p1.value, p2.value, (t - p1.time) / (p2.time - p1.time));

			case Interpolation::CubicHermite:
				d0 = p1.slope;
				d1 = (p2.type == Interpolation::CubicHermite)
					? p2.slope
					: getSlope(p1, p2, _Specialization());
				return math::cubicHermiteInterpolate(
					p1.time, p1.value, p2.time, p2.value, d0, d1, t);

			case Interpolation::Discrete:
				return p1.value;

			default:
				ANIP_FAIL("Invalid interpolation type");
			}
		}

		template<typename U = T>
		static U _interpolate(const Keypoint& p1, const Keypoint& p2, float t, _Fallback)
		{
			return p1.value;
		}

		template<typename U = T>
		static U _extrapolate(const Keypoint& p, float t, _Specialization, _HasArithmetic(U))
		{
			return p.value + p.slope * (t - p.time);
		}

		template<typename U = T>
		static U _extrapolate(const Keypoint& p, float t, _Fallback)
		{
			return p.value;
		}

		void _copyFrom(const KeyedValue& v)
		{
			if (v._head == nullptr) return;
			auto i = new Keypoint(*v._head);
			_head = i;
			while (i->_next != nullptr)
			{
				auto j = new Keypoint(*i->_next);
				i->_next = j;
				j->_prev = i;
				i = j;
			}
			_tail = i;
			_size = v._size;
		}

	public:
		TemporalTiling tiling = TemporalTiling::Clamp;

		KeyedValue() {}

		KeyedValue(T def)
		{
			auto pt = new Keypoint(0, def);
			_head = _tail = pt;
			_size = 1;
		}

		KeyedValue(std::initializer_list<std::pair<float, T>> vals,
			Interpolation interp = Interpolation::Linear)
		{
			for (auto& [i, j] : vals)
			{
				addKey({ i, j, interp });
			}
		}

		~KeyedValue()
		{
			clear();
		}

		KeyedValue(const KeyedValue& v)
		{
			_copyFrom(v);
		}

		KeyedValue& operator=(const KeyedValue& v)
		{
			clear();
			_copyFrom(v);
			return *this;
		}

		static KeyedValue fromFunction(
			std::function<T(float)> func, float step = 0.01f, 
			float start = 0, float end = 1,
			Interpolation interp = Interpolation::Linear)
		{
			KeyedValue val;
			float t = start;
			int i = 0;
			do
			{
				val.addKey({ start + i * step, func(t), interp });
				t = start + (++i) * step;
			} while (t < end - 0.001f);
			val.addKey({ end, func(end) });
			return val;
		}

		void clear()
		{
			Keypoint *i = _head, *j;
			while (i != nullptr)
			{
				j = i->_next;
				delete i;
				i = j;
			}
			_head = _tail = nullptr;
			_size = 0;
		}

		const Keypoint* first() const { return _head; }
		const Keypoint* last() const { return _tail; }
		const size_t size() const { return _size; }

		T operator[](float t) const
		{
			assert(_head != nullptr);

			if (t > _tail->time || t < _head->time)
			{
				float len = _end - _start;
				switch (tiling)
				{
				case TemporalTiling::None:
					return T();
				case TemporalTiling::Clamp:
					return t > _end 
						? _tail->value 
						: _head->value;
				case TemporalTiling::Extrapolate:
					return t > _end
						? _extrapolate(*_tail, t, _Specialization())
						: _extrapolate(*_head, t, _Specialization());
				case TemporalTiling::Repeat:
					if (t < _start) t = len - fmod(_start - t, len) + _start;
					else t = fmod(t - _start, len) + _start;
					break;
				case TemporalTiling::Mirror:
					if (t < _start) t = 2 * len - fmod(_start - t, 2 * len) + _start;
					if ((int)((t - _start) / len) % 2 == 0)
						t = fmod(t - _start, len) + _start;
					else
						t = len - fmod(t - _start, len) + _start;
					break;
				}
			}

			Keypoint* i = _head;
			for (; i != nullptr && i->time < t; i = i->_next);

#pragma warning(suppress: 6011)
			if (i == _head) return i->value;
			if (i == nullptr) return _tail->value;

			const Keypoint& p1 = *i->_prev;
			const Keypoint& p2 = *i;
			
			return _interpolate(*i->_prev, *i, t, _Specialization());
		}

		const Keypoint& keepLastKey(float t)
		{
			assert(_head != nullptr);
			
			Keypoint* i = _head;
			for (; i->_next != nullptr && i->time < t; i = i->_next);
			if (i == _head)
				throw std::logic_error("No previous key");
			if (i->_next != nullptr)
				i = i->_prev;

			auto pt = new Keypoint(*i);
			pt->time = t;

			pt->_prev = i;
			pt->_next = i->_next;
			if (i->_next == nullptr)
			{
				_tail = pt;
				i->_next = pt;
			}
			else
			{
				i->_next->_prev = pt;
				i->_next = pt;
			}
			_size++;
			return *pt;
		}

		void deleteKey(const Keypoint& pt)
		{
			if (&pt == _head)
			{
				_head = pt._next;
				if (_head == nullptr)
					_start = _end = _tail == nullptr ? 0 : _tail->time;
				else
					_start = _head->time;
			}
			else
				pt._prev->_next = pt._next;
			if (&pt == _tail)
			{
				_tail = pt._prev;
				if (_tail == nullptr)
					_end = _start = _head == nullptr ? 0 : _head->time;
				else
					_end = _tail->time;
			}
			else
				pt._next->_prev = pt._prev;

			delete& pt;
			_size--;
		}

		const Keypoint& addKey(const Keypoint& pt)
		{
			_size++;
			auto newpt = new Keypoint(pt);

			if (_head == nullptr)
			{
				_head = _tail = newpt;
				newpt->_prev = newpt->_next = nullptr;
				_start = _end = pt.time;
				return *newpt;
			}

			auto i = _head;
			for (; i != nullptr && i->time <= pt.time; i = i->_next);
			if (i == nullptr)
			{
				_tail->_next = newpt;
				newpt->_prev = _tail;
				newpt->_next = nullptr;
				_tail = newpt;
				_end = newpt->time;
				return *newpt;
			}
			else if (i == _head)
			{
				_head->_prev = newpt;
				newpt->_next = _head;
				newpt->_prev = nullptr;
				_head = newpt;
				_start = newpt->time;
				return *newpt;
			}
			else
			{
				newpt->_prev = i->_prev;
				newpt->_next = i;
				i->_prev->_next = newpt;
				i->_prev = newpt;
				return *newpt;
			}
		}

		void smoothenSlope(const Keypoint& kp, float fac = 1.0f) const
		{
			_smoothen(kp, fac, _Specialization());
		}
	};

	typedef KeyedValue<float> kfloat;
	typedef KeyedValue<vec2> kvec2;
	typedef KeyedValue<color> kcolor;

	template<typename T>
	void binaryWrite(const KeyedValue<T>& val, std::ostream& o)
	{
		binaryWrite<TemporalTiling>(val.tiling, o);
		binaryWrite<size_t>(val.size(), o);
		for (auto k = val.first(); k != nullptr; k = k->next())
		{
			binaryWrite<float>(k->time, o);
			binaryWrite<float>(k->weight, o);
			binaryWrite<Interpolation>(k->type, o);
			binaryWrite<T>(k->slope, o);
			binaryWrite<T>(k->value, o);
		}
	}

	template<typename T>
	void binaryRead(KeyedValue<T>& val, std::istream& in)
	{
		typename KeyedValue<T>::Keypoint *_last = nullptr;
		binaryRead(val.tiling, in);
		size_t n;
		binaryRead(n, in);
		val.clear();
		val._size = n;
		
		for (size_t i = 0; i < n; i++)
		{
			auto kp = new typename KeyedValue<T>::Keypoint;
			binaryRead(kp->time, in);
			binaryRead(kp->weight, in);
			binaryRead(kp->type, in);
			binaryRead<T>(kp->slope, in);
			binaryRead<T>(kp->value, in);
			if (_last == nullptr)
				val._head = kp;
			else
			{
				_last->_next = kp;
				kp->_prev = _last;
			}
			_last = kp;
		}
		val._tail = _last;
		val._start = val._head->time;
		val._end = val._tail->time;
	}
}

#undef _HasArithmetic